Skip to content

Add ad-hoc AI credit pack option to onboarding "Choose how to start" - #14605

Merged
jefflloyd merged 12 commits into
masterfrom
factory/rev-1886-onboarding-credit-packs
Aug 3, 2026
Merged

Add ad-hoc AI credit pack option to onboarding "Choose how to start"#14605
jefflloyd merged 12 commits into
masterfrom
factory/rev-1886-onboarding-credit-packs

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Free users who don't want a subscription had no way to get AI credits during onboarding. The Choose how to start offer (OfferVariant::ChooseHowToStart) only forked between subscribing and "Set up AI later", so anyone who just wanted to try Warp had to either commit to a plan or start with no AI at all.

This adds a third option that buys a one-time add-on credit pack.

Buy AI credits card. A new OfferChoice::BuyCredits sits between the subscribe and skip cards and presents the server's credit packs as a single horizontal row of tiles — credit count, the price actually charged, and the volume savings vs the smallest pack. Tiles are click-selectable and the three cards are arrow-key navigable. The layout follows the Billing & Usage page's add-on credit denominations row (compact credit chips 8px apart, with the same credits icon); the tiles are Expanded so the packs always stay on one line rather than wrapping.

Pricing is sourced at runtime, never hardcoded. ai::onboarding::onboarding_credit_packs builds the list from pricingInfo.addonCreditsOptions and the viewer's PurchaseAddOnCreditsPolicy::effective_premium_bps, reusing price_usd_cents_with_premium so the displayed price matches what the server charges (free plan: $12 / $24 / $60 / $120 for the 400 / 1,000 / 3,000 / 6,500 packs). The option is hidden entirely when pricing hasn't loaded or the plan can't buy packs, and the packs refresh when pricing or the user's teams change.

Purchase and checkout return. "Get Warping" on that option calls purchaseAddonCredits, billed to whichever team the window is scoped to (usually None during onboarding, which lets the server resolve or create the buyer's personal team). A PurchaseAddonCreditsCheckoutOutput opens the checkout URL and leaves the user on the slide; onboarding advances once the server's aiCreditAvailability decision says the user can actually make an AI request. That is the whole bar — onboarding doesn't care how the user got access, only that it never lets through someone who still can't use AI. Because this offer is only shown to users with no base credits, a brand-new account that cancels checkout keeps reporting unavailable and stays on the slide. A synchronous charge advances immediately; a rejection surfaces a retryable error.

Copy. The subscribe card is titled "Subscribe to a Warp plan". When the credit packs are on screen it adds the savings-framed line save 20% on add-on credits with any Build plan (matching the web wording, no "surcharge" language); when they aren't, it keeps its original copy.

Demo binary. crates/onboarding's standalone demo can now preview the post-auth offer slides via ONBOARDING_DEMO_OFFER=choose_how_to_start|head_start. It also had to start initializing feature flags — a debug build previously panicked on the first FeatureFlag::is_enabled check before any window rendered, so the demo was unusable for these slides.

Scoping note on ai_access_slide

Triage flagged whether "Get AI access" (ai_access_slide.rs) needs the same third option. It does not, and this PR deliberately leaves it alone: that slide is only constructed on the non-account-first path (agent_onboarding_view.rs builds ai_access_slide when AccountFirstOnboarding is disabled and offer_slide when it is enabled), so the two are alternative flows rather than forks that must stay in sync.

Rework changes

Round 1 — requester feedback on the first implementation.

  • Card renamed. "Use Warp with AI" no longer distinguished the card once two of the three options were ways to use Warp with AI, so it is now "Subscribe to a Warp plan". The telemetry action name (use_warp_with_ai) is deliberately not renamed so existing dashboards keep continuity — called out in a code comment.
  • Packs on one line. The pack selector previously stacked four full-width rows, which made the slide tall enough for the onboarding container to scroll. They now render as one horizontal row of equal-width tiles modelled on Billing & Usage. Each tile still shows all three data points; the only detail shed is the redundant "credits" word after each number, so CreditPackOption::credits_label() returns just the thousands-separated count ("6,500").

Round 2 — review finding on the advance predicate.

  • The checkout-pending state advanced whenever has_any_ai_remaining() became true, which is broader than "the pack the user bought was granted", so a user with pre-existing AI availability could cancel checkout and still be advanced. The check moved to the purchased/add-on credit balance.

Round 3 — requester feedback on this PR (current).

  • The add-on savings line is gated on the packs actually rendering. primary_description now takes the same shows_credit_packs condition that decides whether the buy-credits card renders, rather than a second predicate that could drift, and falls back to exactly the previous copy when the packs aren't shown.
  • The intermediate purchase status strings are gone. "Starting your purchase…" and "Finish your purchase in the browser…" are deleted, not left dead; the "Waiting for checkout…" button label is now the only status the user sees. One deliberate exception: the failure line is kept, because it is terminal rather than transient — without it a rejected purchase would fail silently, with the button simply snapping back to "Get Warping". Happy to drop it too if you'd rather.
  • All four pack tiles are the same height. The smallest pack has no volume discount and so rendered shorter. Every tile now lays out a badge slot; the no-discount tile lays out the same text fully transparent, which reserves exactly the right line box without a fixed-height constant.
  • The advance rule is relaxed from "balance grew" to "balance is non-zero" (partially reverting round 2 per your note that it needn't monotonically increase). It stays scoped to purchased add-on credits rather than reverting to has_any_ai_remaining, so a brand-new zero-credit user who cancels still stays put while someone who already has credits can continue. canceled_checkout_does_not_advance_a_user_who_already_had_credits asserted the now-unwanted behavior and was replaced by canceled_checkout_does_not_advance_a_user_with_no_credits plus a_user_who_already_has_credits_can_continue.

Round 4 — requester review comments on this PR. Plumbing and comments only; nothing on screen changes, so no new visual proof was captured.

  • The advance signal is now the server's aiCreditAvailability, not a client-side balance. [REV-1714] Use server-authoritative AI credit availability in the client #14634 landed on master while this was in review; that branch is merged in and purchased_credit_balance() is deleted. The subscription listens for AIRequestUsageModelEvent::CreditAvailabilityUpdated and reads AIRequestUsageModel::server_availability().available. on_purchased_credit_balance_observed(i32)on_ai_credit_availability_observed(bool) on the view, on_credit_balance_observedon_credit_availability_observed(bool) on the model. This also fixes a case the balance version got wrong: access arriving from a team plan mid-checkout now completes.
  • Onboarding credit helpers moved out of root_view into app/src/ai/onboarding.rs, next to the other onboarding AI helpers. root_view keeps only the subscriptions and the event relay. (Two of the three extracted helpers were one-liners and were inlined again in round 5 — only onboarding_credit_packs remains.)
  • The purchase is billed to the window's current team via UserWorkspaces::team_uid_for_window instead of a hardcoded None, so team-discovery and domain-capture signups resolve to the team they landed on.
  • The extra refresh_request_usage on app re-activation is removed — availability rides along on the workspace-metadata query that the same handler already refreshes.
  • Two comment rewrites: the pack-refresh comment now says the purchase policy comes from the user's workspace, and the demo binary's feature-flag initialization comment was rewritten from scratch.
  • One thread is left open as a question: whether the demo binary's ONBOARDING_DEMO_OFFER env var should stay. It's gated behind required-features = ["bin"] and never compiled into the app.

Round 5 — requester review comments on this PR (current). Refactor and comment deletions only; nothing on screen changes, so again no new visual proof was captured.

  • The two one-line helpers extracted in round 4 are inlined again. onboarding_purchase_team_uid was a single delegating line under a five-line comment, so the call site now just reads UserWorkspaces::as_ref(ctx).team_uid_for_window(ctx.window_id()).
  • The advance check delegates to has_any_ai_remaining instead of the bespoke has_ai_credit_availability wrapper. Post-[REV-1714] Use server-authoritative AI credit availability in the client #14634 that method already short-circuits on the server decision via server_availability_permits_ai, so it reads the same answer — and it is strictly better in one case: an OutOfCredits user with a usable BYOK/Bedrock path now advances instead of being trapped on the slide, which matches the "can they make an AI request" bar.
  • Three over-explaining comments deleted (the team-uid fetch, the workspace-metadata refresh) and the on_credit_availability_observed doc comment cut from ten lines to four.
  • Net effect is −45 lines with no behaviour change beyond the BYOK case above.

Verification

Regression tests (new; the pricing math test is the guard on the confirmed server SKUs):

  • app/src/pricing/pricing_tests.rs — 5 tests pinning the premium math ($10/$20/$50/$100 at list, $12/$24/$60/$120 at 2000 bps), volume savings 0/20/33/38%, empty input, and the no-negative-savings edge case.
  • crates/onboarding/src/model_tests.rs — 11 tests over the purchase state machine, including the cancelled-checkout case (repeated unavailable answers hold the slide), access arriving from any source completing the purchase, and that the availability callback is inert with no checkout pending.
  • crates/onboarding/src/slides/offer_slide_tests.rs — 7 tests over the slide: the third option appears only for the free-standard offer and only with packs, the subscribe copy with and without packs, arrow-key navigation, get_warping_buys_credits_when_the_credit_option_is_selected, "Set up AI later" still emitting while checkout is pending, and pack-list truncation.

Repository checks: ./script/format; cargo clippy -p onboarding --all-targets --tests -- -D warnings; cargo clippy -p warp --all-targets --tests -- -D warnings; cargo test -p onboarding (44 passed); cargo test -p warp --lib pricing:: (5 passed); cargo test -p warp --lib root_view (11 passed); cargo test -p warp --lib ai::request_usage (49 passed, covering the delegated has_any_ai_remaining behaviour). All re-run after this rework and after merging the current master into this branch.

Visual verification (full-desktop screenshots and a recording below), run against the real UI via ONBOARDING_DEMO_OFFER=choose_how_to_start ./target/debug/onboarding:

  • The three cards read "Subscribe to a Warp plan" (with the green Recommended pill), "Buy AI credits" and "Set up AI later".
  • The four packs sit side by side on one line400 / $12, 1,000 / $24 Save 20%, 3,000 / $60 Save 33%, 6,500 / $120 Save 38% — and all four tile boxes are now the same height, with the 400 tile's blank badge slot bringing its bottom border level with the others.
  • The entire slide — heading, all three cards and the bottom buttons — is visible at once with no scrollbar and nothing clipped.
  • Starting a purchase leaves the user on the slide, the button becomes a disabled "Waiting for checkout…", and no status paragraph appears anywhere under or between the cards.
  • Clicking the right-most pack moves the tile highlight; "Set up AI later" still advances.
  • The no-packs fallback copy can't be exercised in the demo (it always supplies packs), so it is covered by subscribe_copy_drops_the_add_on_line_when_no_packs_are_shown instead.

Originating thread: https://warpdev.slack.com/archives/C0BDQDW8V5E/p1785523238898989

Computer-use screenshots

Full-desktop, uncropped captures with the app window surrounded by desktop, so the absence of clipping and of any scrollbar is visible rather than asserted. The demo runs under Xvfb with no window manager, so the window has no OS title bar and cannot be resized; the surrounding desktop and taskbar show its full extent.

Default state: heading, all three cards and the Back / Get Warping buttons in one frame. The four pack tiles sit on one row — 400/$12, 1,000/$24 Save 20%, 3,000/$60 Save 33%, 6,500/$120 Save 38% — and all four tile boxes are the same height, the 400 tile carrying a blank badge slot.
After starting a purchase: the screen stays put, the primary button reads a greyed-out "Waiting for checkout…", and no status paragraph appears under or between the cards.
After clicking the right-most pack: the 6,500/$120 tile takes the highlight.

Computer-use video recording

Continuous full-screen recording: default state, starting a purchase via the primary button, and selecting the last credit pack.

Conversation: https://staging.warp.dev/conversation/0bec429a-a2ca-46af-892b-4ce333e400ee
Run: https://oz.staging.warp.dev/runs/019fb990-9952-7194-b171-5995e49cc9de

This PR was generated with Oz.

Free users who don't want a subscription had no way to get AI credits
during onboarding: the "Choose how to start" offer only forked between
"Use Warp with AI" (subscribe) and "Set up AI later".

Adds a third option that buys a one-time add-on credit pack:
- `OfferChoice::BuyCredits` renders a card with the server's credit
  packs (credits, premium-adjusted price, volume savings), keyboard
  navigable alongside the other two options.
- Pack prices come from `pricingInfo.addonCreditsOptions` plus the
  viewer's `PurchaseAddOnCreditsPolicy` premium at runtime; nothing is
  hardcoded, and the option is hidden when pricing is unavailable.
- "Get Warping" on that option calls `purchaseAddonCredits` with no
  `teamUid`, so the server resolves or creates a personal team.
- A checkout hand-off opens the URL and leaves the user on the slide;
  onboarding advances only once the granted credits are observed on a
  usage refresh, so abandoning checkout never advances anyone.
- The subscribe card now states the savings-framed add-on discount.

The onboarding demo binary can now preview the post-auth offer slides
via ONBOARDING_DEMO_OFFER, and initializes feature flags so its debug
build can render them at all.

Co-Authored-By: Warp <agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label Jul 31, 2026
oz-agent and others added 2 commits July 31, 2026 20:19
The post-auth offer slide is only constructed on the account-first
onboarding flow, so previewing it from the demo binary panicked at the
`expect("offer slide exists")` in the render match. Turn the flag on
when ONBOARDING_DEMO_OFFER selects an offer to preview.

Co-Authored-By: Warp <agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
@warp-agent-staging
warp-agent-staging Bot marked this pull request as ready for review July 31, 2026 20:24
@warp-agent-staging
warp-agent-staging Bot requested a review from jefflloyd July 31, 2026 20:24
oz-agent and others added 5 commits July 31, 2026 20:50
Requester feedback on the onboarding offer slide:

- "Use Warp with AI" no longer distinguishes the card now that two of
  the three options are ways to use Warp with AI, so it is now
  "Subscribe to a Warp plan". The telemetry action name is deliberately
  unchanged so existing dashboards keep continuity.
- The stacked pack rows made the slide tall enough to scroll. The four
  packs now sit on a single horizontal row of equal-width tiles,
  following how the Billing & Usage page presents add-on credit
  denominations (compact credit chips 8px apart, with the credits
  icon). Each tile keeps its credit count, premium-adjusted price and
  savings badge; the count drops the redundant "credits" suffix, which
  the card title and icon already supply.

Co-Authored-By: Warp <agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
The checkout-pending onboarding state advanced whenever
`has_any_ai_remaining()` became true. That predicate is broader than
"the pack the user just tried to buy was granted" — it is also true for
base plan requests, BYOK credentials, overages and auto-reload. A user
who already had AI availability could open checkout, cancel, and still
be advanced, which breaks the requirement that abandoning checkout
leaves them on the slide.

`AwaitingCheckout` now carries the purchased-credit balance observed
just before checkout opened, and the new `on_credit_balance_observed`
completes the purchase only when that balance has actually grown. The
app supplies the balance from the user- and workspace-scoped bonus
grants that a pack purchase lands in, rather than any-AI-availability.

Co-Authored-By: Warp <agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
Four adjustments from the PR review:

- The subscribe card's add-on savings line only makes sense beside the
  packs it refers to, so `primary_description` now takes the same
  `shows_credit_packs` condition that decides whether the buy-credits
  card renders, and falls back to the original copy otherwise. Reusing
  that one condition avoids a parallel predicate that could drift.
- The transient "Starting your purchase…" and "Finish your purchase in
  the browser…" lines are gone, along with their strings; the
  "Waiting for checkout…" button label is the only status the user
  sees. The failure line stays: it is terminal rather than transient,
  and without it a rejected purchase would fail silently.
- Every pack tile now renders a badge slot so all four are the same
  height. The smallest pack has no volume discount, so its slot lays
  out the same text fully transparent, reserving the right line box
  without a fixed-height constant.
- The advance-after-checkout rule is relaxed from "the balance grew" to
  "the balance is non-zero". A brand-new account with no credits who
  cancels still stays on the slide, while someone who already holds
  credits can continue. The balance stays scoped to purchased add-on
  credits rather than general AI availability, which would also count
  base free-plan requests and BYOK and so would advance a brand-new
  user who cancelled.

Co-Authored-By: Warp <agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
Comment thread app/src/root_view.rs Outdated
Comment thread app/src/root_view.rs Outdated
Comment thread app/src/root_view.rs Outdated
Comment thread app/src/root_view.rs Outdated
Comment thread app/src/root_view.rs Outdated
Comment thread app/src/root_view.rs Outdated
Comment thread crates/onboarding/src/bin/main.rs
Comment thread crates/onboarding/src/bin/main.rs Outdated
…ility

Review feedback from the PR:
- Move the onboarding credit helpers out of `root_view` into
  `app/src/ai/onboarding.rs`, next to the other onboarding AI helpers.
- Gate the post-checkout advance on the server's `aiCreditAvailability`
  decision instead of reconstructing a purchased-credit balance on the
  client. Onboarding only needs to know the user can make an AI request.
- Bill the purchase to the team the window is scoped to instead of
  hardcoding `None`, so team-discovery/domain-capture signups resolve.
- Drop the extra `refresh_request_usage` on app re-activation now that
  availability rides along on the workspace-metadata query.
- Comment wording fixes in `root_view` and the onboarding demo binary.

Co-Authored-By: Oz <oz-agent@warp.dev>
@warp-agent-staging
warp-agent-staging Bot requested a review from jefflloyd August 3, 2026 03:13
Comment thread app/src/ai/onboarding.rs Outdated
Comment thread app/src/ai/onboarding.rs Outdated
Comment thread app/src/root_view.rs Outdated
Comment thread app/src/root_view.rs Outdated
…ny_ai_remaining

Review feedback from the PR:
- Inline `onboarding_purchase_team_uid`; it was a one-line wrapper with a
  five-line comment.
- Delegate the onboarding advance check to `has_any_ai_remaining`, which
  is already server-authoritative post-#14634, and drop the redundant
  `has_ai_credit_availability` wrapper.
- Remove the over-explaining comments on the team-uid fetch and the
  workspace-metadata refresh, and trim the availability doc comment.

Co-Authored-By: Oz <oz-agent@warp.dev>
@warp-agent-staging
warp-agent-staging Bot requested a review from jefflloyd August 3, 2026 15:23
@jefflloyd
jefflloyd merged commit c7e3c4a into master Aug 3, 2026
47 of 49 checks passed
@jefflloyd
jefflloyd deleted the factory/rev-1886-onboarding-credit-packs branch August 3, 2026 17:19
warp-agent-staging Bot pushed a commit that referenced this pull request Aug 5, 2026
…V-1939)

Put the third "Buy AI credits" option added in #14605 behind the
server-assigned ONBOARDING_CHOOSE_HOW_TO_START experiment so the
two-option and three-option layouts can be compared.

- Map the server's ONBOARDING_CHOOSE_HOW_TO_START_CONTROL and
  ONBOARDING_CHOOSE_HOW_TO_START_THREE_OPTIONS arms through the Cynic
  enum, the schema snapshot, and ServerExperiment, and resolve them to
  an explicit Control / Experiment / Unassigned value.
- Snapshot that arm onto the onboarding view immediately before the
  post-auth offer is shown, then leave it frozen for that exposure.
- Show credit packs only for the experiment arm with a non-empty pack
  list; every other case renders the historical two-option screen with
  the exact pre-#14605 copy.
- Attach experiment_arm to the offer's slide view, action, upgrade
  start/completion, and terminal completion events, and add
  onboarding_credit_purchase_started / _completed.

Co-Authored-By: Warp Agent <agent@warp.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants